package se.webbzon.oschi01.worldarea;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.SortedSet;
import java.util.TreeSet;

import javax.media.j3d.Transform3D;
import javax.vecmath.AxisAngle4d;
import javax.vecmath.Vector3d;

import se.webbzon.boltzmann.configuration.Controls;
import se.webbzon.boltzmann.configuration.Graphics;
import se.webbzon.boltzmann.debug.Debug;
import se.webbzon.oschi01.friendly.Friendly;
import se.webbzon.oschi01.worldcamera.WorldCameraPane;
import se.webbzon.oschi01.worldobject.WorldGroup;
import se.webbzon.oschi01.worldobject.WorldObject;
import se.webbzon.oschi01.worldobject.WorldObjectException;
import se.webbzon.oschi01.worldobject.WorldStaticObject;

public class WorldArea {
	
	// The properties that can be set for the world area
	public static final int RENDERING_ORDER = (1<<0);
	
	// The number of iteration used in the method moveContact() (default = 10)
	private static final int moveContactIterations = 10;
	
	// The properties of this world area
	private final int properties;
	
	private final WorldAppearance3D appearance;
	
	// A collection of listeners to this world area
	private final Collection<WorldAreaListener> listeners;
	
	// A collection of all instances in this world area
	private final Collection<WorldObject> instances;
	
	// Collections used to speed up global stepping and drawing
	private final Collection<WorldObject> onStepBeginRegister;
	private final Collection<WorldObject> onStepRegister;
	private final Collection<WorldObject> onStepEndRegister;
	private final Collection<WorldObject> onAreaEnterRegister;
	private final Collection<WorldObject> onAreaLeaveRegister;
	private final SortedSet<WorldObject> onPreDrawRegister;
	private final SortedSet<WorldObject> onPostDrawRegister;
	
	// A collection of created and destroyed instances which should
	// be added or removed in the end of the current step
	private static enum State {IDLE,CREATE,STEP,DESTROY};
	private State currentState;
	private Object lock = new Object();
	private final Collection<WorldObject> createdInstances;
	private final Collection<WorldObject> destroyedInstances;
	
	// Destroys all instances in the room in the end of the global
	// step if set
	private boolean destroy;
	
	/*============================================================
	Constructors
	============================================================*/
	
	/** Creates a new empty world area. **/
	public WorldArea(int worldAreaProperties) {
		properties = worldAreaProperties;
		listeners = new ArrayList<WorldAreaListener>();
		instances = new HashSet<WorldObject>();
		
		onStepBeginRegister = new HashSet<WorldObject>();
		onStepRegister = new HashSet<WorldObject>();
		onStepEndRegister = new HashSet<WorldObject>();
		onAreaEnterRegister = new HashSet<WorldObject>();
		onAreaLeaveRegister = new HashSet<WorldObject>();
		
		final DepthComparator cmp = new DepthComparator();
		onPreDrawRegister = Collections.synchronizedSortedSet(
											new TreeSet<WorldObject>(cmp));
		onPostDrawRegister = Collections.synchronizedSortedSet(
											new TreeSet<WorldObject>(cmp));
		
		createdInstances = Collections.synchronizedCollection(new ArrayList<WorldObject>());
		destroyedInstances = Collections.synchronizedCollection(new ArrayList<WorldObject>());
		currentState = State.IDLE;
		destroy = false;
		
		appearance = new WorldAppearance3D(worldAreaProperties,this);
	}
	
	/*============================================================
	Public Methods
	============================================================*/
	
	/** Returns the properties for this world area. **/
	public int getProperties() {
		return properties;
	}
	
	public WorldAppearance3D getAppearance() {
		return appearance;
	}
	
	/** Returns true if the world area is empty. **/
	public boolean isEmpty() {
		synchronized (lock) {
			switch (currentState) {
			case IDLE:
			case CREATE:
				return instances.isEmpty() && createdInstances.isEmpty();
			default:
				return instances.size() + createdInstances.size() - destroyedInstances.size() == 0;
			}
		}
	}
	
	/** Destroys all the instances in this world area. **/
	public void destroyAll() {
		synchronized (lock) {
			if (currentState == State.IDLE) {
				removeAndDeregisterAll();
			} else
				destroy = true;
		}
	}
	
	/** Adds a listener to this world area. **/
	public boolean addListener(WorldAreaListener listener) {
		int d;
		Debug.debugCollection(this, listeners);
		return listeners.add(listener);
	}
	
	/** Removes a listener from this world area. **/
	public boolean removeListener(WorldAreaListener listener) {
		return listeners.remove(listener);
	}
	
	/** Removes all listeners from this world area. **/
	public void clearListeners() {
		listeners.clear();
	}
	
	/** Sends a request to the engine associated with this world area
	 * that the engine should switch to a given destination. **/
	public void gotoWorldArea(WorldArea destination) {
		notifyGotoRequest(destination);
	}
	
	/** Adds (and creates) an instance to this world area. The
	 * instance will exist in the world area in the beginning 
	 * of the next global step. **/
	public boolean createInstance(WorldObject instance) {
		synchronized (lock) {
			if (!instance.create(this))
				throw new WorldObjectException("The instance is already created!");
			if (currentState == State.STEP)
				return createdInstances.add(instance);
			addAndRegisterInstance(instance);
			return true;	
		}
	}
	
	/** Removes (and destroys) an instance from this world area.
	 * The instance will be removed in the end of the global step. **/
	public boolean destroyInstance(WorldObject instance) {
		synchronized (lock) {
			if (!instance.exists())
				return true;
			if (!instance.destroy(this))
				throw new WorldObjectException("The instance does not exists in this world area!");
			else if (currentState == State.STEP)
				return destroyedInstances.add(instance);
			removeAndDeregisterInstance(instance);
			return true;	
		}
	}
	
	/** Returns a collection of all instances in this world area which
	 * are an instance of the given class. **/
	@SuppressWarnings("unchecked")
	public <T> Collection<T> findInstances(Class<T> objectClass) {
		synchronized (lock) {
			Collection<T> instances = new ArrayList<T>();
			for (WorldObject instance : this.instances)
				if (objectClass.isInstance(instance))
					instances.add((T) instance);
			return instances;
		}
	}
	
	/** Returns true if a given instance can be scaled, moved and rotated
	 * under the given circumstances without colliding with another instance
	 * in the world area. If position, scaling or rotation equals null then
	 * the parameter is ignored. **/
	public boolean placeFree(WorldStaticObject self, 	Vector3d position,
														Vector3d scaling, 
														AxisAngle4d rotation) {
		if (self.hasSolidMask() == false)
			return true;

		// Move the mask temporary
		final Transform3D oldTransform = new Transform3D();
		self.getTransform(oldTransform);
		final Transform3D newTransform = new Transform3D(oldTransform);
		if (position != null)
			newTransform.setTranslation(position);
		if (scaling != null)
			newTransform.setScale(scaling);
		if (rotation != null)
			newTransform.setRotation(rotation);
		
		final WorldGroup group = self.getGroup();
		if (group != null)
			group.transform(newTransform);
		
		self.getMask().setTransform(newTransform);
		
		boolean free = true;
		// Check for collisions
		for (WorldObject instance : instances)
			if (instance instanceof WorldStaticObject && !instance.equals(self)) {
				WorldStaticObject other = (WorldStaticObject) instance;
				if (other.hasSolidMask() && 
						self.getMask().intersects(other.getMask())) {
					free = false;
					break;
				}
			}
		
		// Move the mask back to its original position
		self.getMask().setTransform(oldTransform);
		return free;
	}
	
	/** Returns true if the masks of two instances collides when the
	 * first given instance is scaled, moved and rotated under the
	 * given circumstances. If position, scaling or rotation equals 
	 * null then the parameter is ignored. **/
	public boolean placeMeeting(WorldStaticObject self, WorldStaticObject other,
								Vector3d position, Vector3d scaling,
								AxisAngle4d rotation) {
		if (other.hasSolidMask() == false || self.hasSolidMask() == false)
			return false;
		
		// Move the mask temporary
		final Transform3D oldTransform = new Transform3D();
		self.getTransform(oldTransform);
		final Transform3D newTransform = new Transform3D(oldTransform);
		if (position != null)
			newTransform.setTranslation(position);
		if (scaling != null)
			newTransform.setScale(scaling);
		if (rotation != null)
			newTransform.setRotation(rotation);
		
		final WorldGroup group = self.getGroup();
		if (group != null)
			group.transform(newTransform);
		
		self.getMask().setTransform(newTransform);
		
		final boolean meeting = self.getMask().intersects(other.getMask());
		
		// Move the mask back to its original position
		self.getMask().setTransform(oldTransform);
				
		return meeting;
	}
	
	/** Returns a collection of all instances in the world area that the given
	 * instance will collide with if the masks of the given instance is 
	 * scaled, moved and rotated under the given circumstances. 
	 * If position, scaling or rotation equals null then the parameter is ignored. **/
	public Collection<WorldStaticObject> placeMeetingAll(WorldStaticObject self, 	
															Vector3d position,
															Vector3d scaling, 
															AxisAngle4d rotation) {
		ArrayList<WorldStaticObject> all = new ArrayList<WorldStaticObject>();
		
		if (self.hasSolidMask() == false)
			return all;

		// Move the mask temporary
		final Transform3D oldTransform = new Transform3D();
		self.getTransform(oldTransform);
		
		final Transform3D newTransform = new Transform3D(oldTransform);
		if (position != null)
			newTransform.setTranslation(position);
		if (scaling != null)
			newTransform.setScale(scaling);
		if (rotation != null)
			newTransform.setRotation(rotation);
		
		final WorldGroup group = self.getGroup();
		if (group != null)
			group.transform(newTransform);
		
		self.getMask().setTransform(newTransform);
		
		// Check for collisions
		for (WorldObject instance : instances)
			if (instance instanceof WorldStaticObject && !instance.equals(self)) {
				WorldStaticObject other = (WorldStaticObject) instance;
				if (other.hasSolidMask() && 
						self.getMask().intersects(other.getMask())) {
					all.add(other);
				}
			}
		
		// Move the mask back to its original position
		self.getMask().setTransform(oldTransform);
		
		return all;
	}
	
	/** Moves an instance in the given direction (defined by the given vector)
	 * until the direction is no longer free. The maximal distance the instance
	 * will be moved is given by the input "maxDistance".
	 * Notice that this method is very slow so use with care.**/
	public void moveContact(WorldStaticObject self, Vector3d direction,
							double maxDistance) {
		// Normalize the direction vector
		direction.normalize();
		// A "safe" distance the object can be moved without colliding
		double safeDistance = 0;
		// The current distance
		double distance = maxDistance/2;
		maxDistance /= 4;
		
		final Vector3d position = self.getTranslation();
		final double xs = position.x, ys = position.y, zs = position.z;
		for (int i = 0;i < moveContactIterations; i++) {
			position.set(	xs + direction.x * distance,
							ys + direction.y * distance,
							zs + direction.z * distance);
			if (placeFree(self,position,null,null)) {
				safeDistance = distance;
				distance += maxDistance;
			} else
				distance -= maxDistance;
			maxDistance /= 2;
		}
		// Move the instance to the safe position
		self.setPosition(	xs + direction.x * safeDistance, 	
							ys + direction.y * safeDistance, 
							zs + direction.z * safeDistance);
	}
	
	/** Moves an instance in the given direction (defined by the given vector)
	 * until no collision occurs. The maximal distance the instance will be 
	 * moved is given by the input "maxDistance".
	 * Notice that this method is very slow so use with care.**/
	public void moveFree(	WorldStaticObject self, Vector3d direction,
							double maxDistance) {
		direction.normalize();
		direction.scale(maxDistance);
		self.setPosition(	self.getX() + direction.getX(),
							self.getY() + direction.getY(),
							self.getZ() + direction.getZ());
		direction.negate();
		moveContact(self, direction, maxDistance);
	}
	
	/*============================================================
	Friendly Methods
	============================================================*/
	
	/** Returns true if the WorldArea is in an idle state. **/
	@Friendly boolean idling() {
		synchronized (lock) {
			return (currentState == State.IDLE);
		}
	}
	
	/** Performs a global step for this world area. **/
	@Friendly void performStep() {
		synchronized (lock) {
			// Create new instances
			currentState = State.CREATE;
			for (WorldObject instance : createdInstances)
				addAndRegisterInstance(instance);
			createdInstances.clear();
		
			currentState = State.STEP;
			// Perform a global step
			for (WorldObject instance : onStepBeginRegister)
				instance.onStepBegin(this);
			for (WorldObject instance : onStepRegister)
				instance.onStep(this);
			for (WorldObject instance : onStepEndRegister)
				instance.onStepEnd(this);
		
			// Remove old instances
			if (destroy) {
				// Destroy all instances
				destroy = false;
				removeAndDeregisterAll();
				destroyedInstances.clear();
			} else if (!destroyedInstances.isEmpty()) {
				currentState = State.DESTROY;
				// Destroy each object that does not "exist"
				for (WorldObject instance : destroyedInstances)
					removeAndDeregisterInstance(instance);
				destroyedInstances.clear();
			}
			
			currentState = State.IDLE;
		}
	}
	
	/** Performs the onPreDraw event for all instances in this world area. 
	 * The drawing content is projected on the given background. **/
	@Friendly void performPreDraw(WorldCameraPane background) {
		synchronized (onPreDrawRegister) {
			for (WorldObject instance : onPreDrawRegister)
				instance.onPreDraw(this, background);
		}
	}
	
	/** Performs the onPreDraw event for all instances in this world area. 
	 * The drawing content is projected on the given heads-up display. **/
	@Friendly void performPostDraw(WorldCameraPane hud) {
		synchronized (onPostDrawRegister) {
			for (WorldObject instance : onPostDrawRegister)
				instance.onPostDraw(this, hud);
		}
	}
	
	/** Performs the onAreaEnter event for all instances in this world area. **/
	@Friendly void performOnAreaEnter() {
		for (WorldObject instance : onAreaEnterRegister)
			instance.onAreaEnter(this);
	}
	
	/** Performs the onAreaLeave event for all instances in this WorldArea. **/
	@Friendly void performOnAreaLeave() {
		for (WorldObject instance : onAreaEnterRegister)
			instance.onAreaLeave(this);
	}
	
	/*============================================================
	Private Methods
	============================================================*/
	
	/** Returns true if the given property is set in the given properties. **/
	private static final boolean isSet(int properties, int property) {
		return ((properties & property) == property);
	}
	
	/** Adds an instance to the world and adds it to all the registered
	 * collections. **/
	private void addAndRegisterInstance(WorldObject instance) {
		int d;
		Debug.debugCollection(this, instances);
		instances.add(instance);
		
		if ((instance.getRegisteredMethods() & WorldObject.onStepBegin) != 0)
			onStepBeginRegister.add(instance);
			
		if ((instance.getRegisteredMethods() & WorldObject.onStep) != 0)
			onStepRegister.add(instance);
				
		if ((instance.getRegisteredMethods() & WorldObject.onStepEnd) != 0)
			onStepEndRegister.add(instance);
		
		if ((instance.getRegisteredMethods() & WorldObject.onAreaEnter) != 0)
			onAreaEnterRegister.add(instance);
				
		if ((instance.getRegisteredMethods() & WorldObject.onAreaLeave) != 0)
			onAreaLeaveRegister.add(instance);
		
		if ((instance.getRegisteredMethods() & WorldObject.onPreDraw) != 0)
			onPreDrawRegister.add(instance);
		
		if ((instance.getRegisteredMethods() & WorldObject.onPostDraw) != 0)
			onPostDrawRegister.add(instance);
		
		notifyAddInstance(instance);
		
		if ((instance.getRegisteredMethods() & WorldObject.onCreate) != 0)
			instance.onCreate(this);
	}
	
	/** Removes an instance from the world and removes it from all the
	 * registered collections. **/
	private void removeAndDeregisterInstance(WorldObject instance) {
		notifyRemoveInstance(instance);
		
		if ((instance.getRegisteredMethods() & WorldObject.onDestroy) != 0) {
			instance.onDestroy(this);
		}
		
		instances.remove(instance);
		
		if ((instance.getRegisteredMethods() & WorldObject.onStepBegin) != 0)
			onStepBeginRegister.remove(instance);
			
		if ((instance.getRegisteredMethods() & WorldObject.onStep) != 0)
			onStepRegister.remove(instance);
				
		if ((instance.getRegisteredMethods() & WorldObject.onStepEnd) != 0)
			onStepEndRegister.remove(instance);
		
		if ((instance.getRegisteredMethods() & WorldObject.onAreaEnter) != 0)
			onAreaEnterRegister.remove(instance);
				
		if ((instance.getRegisteredMethods() & WorldObject.onAreaLeave) != 0)
			onAreaLeaveRegister.remove(instance);
		
		if ((instance.getRegisteredMethods() & WorldObject.onPreDraw) != 0)
			onPreDrawRegister.remove(instance);
		
		if ((instance.getRegisteredMethods() & WorldObject.onPostDraw) != 0)
			onPostDrawRegister.remove(instance);
		
	}
	
	/** Removes all the instances from the world and also clears all the 
	 * method registers. **/
	private void removeAndDeregisterAll() {
		for (WorldObject instance : instances.toArray(new WorldObject[0])) {
			boolean exists = instance.exists();
			if (exists) {
				instance.destroy(this);
				//notifyRemoveInstance(instance);
				if (isSet(instance.getRegisteredMethods(),WorldObject.onDestroy))
					instance.onDestroy(this);
			}
		}
		notifyClearInstances();
		instances.clear();
		onStepBeginRegister.clear();
		onStepRegister.clear();
		onStepEndRegister.clear();
		onAreaEnterRegister.clear();
		onAreaLeaveRegister.clear();
		onPreDrawRegister.clear();
		onPostDrawRegister.clear();
	}
	
	/** Notifies all listeners that an instance has been added. **/
	private void notifyAddInstance(WorldObject instance) {
		for (WorldAreaListener listener : listeners.toArray(new WorldAreaListener[0]))
			listener.onInstanceAdd(this, instance);
	}
	
	/** Notifies all listeners that an instance has been removed. **/
	private void notifyRemoveInstance(WorldObject instance) {
		for (WorldAreaListener listener : listeners.toArray(new WorldAreaListener[0]))
			listener.onInstanceRemove(this, instance);
	}
	
	/** Notifies all listeners that aall instances has been clear. **/
	private void notifyClearInstances() {
		for (WorldAreaListener listener : listeners.toArray(new WorldAreaListener[0]))
			listener.onInstancesClear(this);
	}
	
	/** Notifies all listeners that a goto request has been invoked. **/
	private void notifyGotoRequest(WorldArea world) {
		for (WorldAreaListener listener : listeners.toArray(new WorldAreaListener[0]))
			listener.onGoto(this, world);
	}
	
	/*============================================================
	Private Classes
	============================================================*/
	
	/** A comparator used to determine the rendering order for 2-dimensional
	 * drawings made by world objects. **/
	private class DepthComparator implements Comparator<WorldObject> {

		@Override public int compare(WorldObject left, WorldObject right) {
			int order = right.getDepth2D()-left.getDepth2D();
			if (order == 0)
				return left.hashCode()-right.hashCode();
			else
				return order;
		}

	}

}
